#!/usr/bin/python3
# -*- coding: utf-8 -*-

from collections import Iterable
from datetime import date, datetime
from os import makedirs
from os.path import abspath, relpath, join as joinpath, normpath, dirname, exists, isfile, isabs
from re import compile as compile_regex, UNICODE, DOTALL

from .callbacks import CallbackLogger
from .envnames import *
from .validators import reject_if_not, reject_if_negative, reject_if_null, reject_if_not_type

_PLACE_HOLDER = compile_regex(r'\$\{([a-z0-9A-Z-_.]+)}', UNICODE | DOTALL)


def ensure_dir(
        dname: str
):
    """ 确保指定的目录存在。

    :param dname: 目录全路径。
    """
    if isfile(dname):
        raise RuntimeError(repr(dname) + ' expected as a directory, but a file found')

    if not exists(dname):
        makedirs(dname, exist_ok=True)


def open_for_write(
        fn: str,
        is_bin: bool = True
):
    """ 打开文件并写入。
    如果该文件所在的目录不存在则创建。

    :param fn: 文件路径。
    :param is_bin: 是否作为二进制文件写入。
    :return: 打开的文件对象。
    """
    ensure_dir(dirname(fn))
    return open(fn, 'wb' if is_bin else 'w')


class Environment(dict):
    """ 执行命令的环境变量集合。
    """

    def __init__(
            self,
            src: dict = None,
            **kwargs
    ):
        """ 构造环境变量集合。

        :param src: 表示环境变量值的字典。
        """
        super().__init__()
        if src:
            self.update(src)
        if kwargs:
            self.update(kwargs)

    def __getitem__(self,
                    key: str
                    ):
        assert key

        return self.getstr(key, '{' + key + '}')

    def getstr(
            self,
            key: str,
            dv: str = ''
    ):
        """ 获取字符串形式的环境变量值。

        :param key: 环境变量的名字。
        :param dv: 环境变量的默认值。
        :return: 环境变量的值。
        """
        r = super().get(key, dv)
        if isinstance(r, str):
            return r
        elif isinstance(r, int):
            return str(r)
        elif isinstance(r, datetime):
            return r.strftime('%Y%m%dT%H%M%S')
        elif isinstance(r, date):
            return r.strftime('%Y%m%d')
        else:
            return str(r)

    def getint(
            self,
            key: str,
            dv: int = 0
    ):
        """ 获取整数形式的环境变量值。

        :param key: 环境变量的名字。
        :param dv: 环境变量的默认值。
        :return: 环境变量的值。
        """
        r = super().get(key, dv)
        return r if isinstance(r, int) else int(r)

    def gettuple(
            self,
            key: str,
            dv: tuple = tuple()
    ):
        """ 获取元组形式的环境变量值。

        :param key: 环境变量的名字。
        :param dv: 环境变量的默认值。
        :return: 环境变量的值。
        """
        r = super().get(key, dv)
        return r if isinstance(r, tuple) else tuple(r)


class Resource(object):
    """执行命令所需的资源接口。"""

    def __init__(self):
        pass

    def close(
            self,
            callback: CallbackLogger
    ):
        """关闭资源。

        :param callback: 用于输出日志的回调接口。
        """
        raise NotImplementedError()


class Command(object):
    """ 命令接口
    """
    __slots__ = ('__description', '__check_required', '__node_dep')

    def __init__(self,
                 description: str = '',
                 check_required: bool = False,
                 node_dep: bool = False
                 ):
        """ 构造Command对象

        :param description: `Command` 对象的描述。
        :param check_required: `Command` 对象是否需要执行完毕后检查结果。
        """
        self.__description = description if description else ''
        self.__check_required = True if check_required else False
        self.__node_dep = True if node_dep else False

    @property
    def description(self) -> str:
        """获取命令的描述。"""
        return self.__description

    @property
    def check_required(self) -> bool:
        """判断命令是否需要检查执行结果。"""
        return self.__check_required

    @property
    def node_dep(self) -> bool:
        """判断命令是否依赖于指定的节点。"""
        return self.__node_dep

    @staticmethod
    def get_resource(
            rtype: type,
            resources: list
    ) -> Resource:
        """根据资源类型查找资源。"""
        reject_if_not_type(rtype, type)
        reject_if_null(resources)

        result = next(filter(lambda res: isinstance(res, rtype), resources), None)
        if not result:
            result = rtype()
            resources.append(result)

        return result

    @staticmethod
    def abspath(
            path: str,
            environment: Environment
    ) -> str:
        """ 通过环境变量 `job.working-directory` 将路径转为绝对路径

        :param path: 原始路径。
        :param environment: 环境变量集合。
        :return: 转换后的绝对路径。
        """
        reject_if_null(environment)

        path = path.strip() if path else ''
        if isabs(path):
            return path

        bp = environment.getstr(JOB_WORKING_DIRECTORY)
        if not bp:
            raise RuntimeError(repr(JOB_WORKING_DIRECTORY) + ' not specified')

        return normpath(joinpath(abspath(bp), path))

    @staticmethod
    def relpath(
            path: str,
            environment: Environment
    ) -> str:
        """ 通过环境变量 `job.working-directory` 将绝对路径转为相对路径

        :param path: 原始路径。
        :param environment: 环境变量集合。
        :return: 转换后的相对路径。
        """
        reject_if_null(environment)

        path = path.strip() if path else ''
        bp = environment.getstr('job.working-directory')
        if not bp:
            bp = '.workspace'

        return relpath(path, bp)

    @staticmethod
    def interpolate(
            s: str,
            environment: Environment
    ) -> str:
        """ 使用环境变量集合对指定的字符串进行插值。

        :param environment: 环境变量集合。
        :param s: 原始字符串。
        :return: 插值后的结果，如果参数`s`是`None`或者空字符串则返回空字符串。
        """

        def repl(m):
            name_ = m.group(1)
            value_ = environment.getstr(name_, '!{' + name_ + '}')
            return value_.replace('\\', '/')

        if not s:
            return ''
        else:
            return _PLACE_HOLDER.sub(repl, s)


class DownloadCommand(Command):
    """ 用于下载包的命令接口。

    """
    __slots__ = ('__src', '__dst',)

    def __init__(self,
                 src: str,
                 dst: str,
                 description: str = '',
                 check_required: bool = False,
                 node_dep: bool = False
                 ):
        """
        构造下载包的命令接口。

        :param src: 下载的源地址。
        :param dst: 下载的目标路径（本地路径）
        """
        super().__init__(description, check_required, node_dep)

        reject_if_not(str)
        reject_if_not(dst)

        self.__src = src
        self.__dst = dst

    @property
    def src(self) -> str:
        return self.__src

    @property
    def dst(self) -> str:
        return self.__dst


class UploadCommand(Command):
    """ 用于上传包的命令接口。

    """
    __slots__ = ('__src', '__dst',)

    def __init__(self,
                 src: str,
                 dst: str,
                 description: str = '',
                 check_required: bool = False,
                 node_dep: bool = False
                 ):
        """
        构造上传包的命令接口。

        :param src: 上传的源地址（本地路径）。
        :param dst: 上传的目标路径。
        """
        super().__init__(description, check_required, node_dep)

        reject_if_not(str)
        reject_if_not(dst)

        self.__src = src
        self.__dst = dst

    @property
    def src(self) -> str:
        return self.__src

    @property
    def dst(self) -> str:
        return self.__dst


class ExecuteCommand(Command):
    """ 用于执行其它程序的命令。

    """
    __slots__ = ('__command_lines', '__redirect_encoding', '__timeout')

    __DEFAULT_TIMEOUT = 15

    def __init__(self,
                 command_lines: (str, Iterable),
                 redirect_encoding: str = None,
                 timeout: int = None,
                 description: str = '',
                 check_required: bool = False,
                 node_dep: bool = False
                 ):
        super().__init__(description, check_required, node_dep)

        if isinstance(command_lines, str):
            command_lines = (command_lines,)
        else:
            command_lines = tuple(command_lines)
        redirect_encoding = redirect_encoding
        timeout = int(timeout) if timeout is not None else self.__DEFAULT_TIMEOUT

        reject_if_not(command_lines)
        reject_if_negative(timeout)

        self.__command_lines = command_lines
        self.__redirect_encoding = redirect_encoding if redirect_encoding else None
        self.__timeout = timeout

    @property
    def command_lines(self) -> tuple:
        return self.__command_lines

    @property
    def redirect_encoding(self) -> str:
        return self.__redirect_encoding

    @property
    def timeout(self) -> int:
        return self.__timeout

    def interpolate_command_lines(
            self,
            environment: Environment,
            command_lines: tuple
    ):
        lb = []
        for line in command_lines:
            # 跳过空行和注释行
            if not line:
                continue
            line = line.strip()
            if not line:
                continue
            if line.startswith('#'):
                continue

            # 检查行尾是否有续行符
            lc = line.endswith('\\')

            if lc:
                lb.append(line[:-1])
            else:
                lb.append(line)
                nline = ' '.join(lb)
                nline = self.interpolate(nline, environment).strip()
                if nline:
                    yield nline
                lb.clear()

        if lb:
            nline = ' '.join(lb)
            nline = self.interpolate(nline, environment).strip()
            if nline:
                yield nline

    def __call__(
            self,
            environment: Environment,
            resources: list,
            callback: CallbackLogger
    ):
        reject_if_null(environment)
        reject_if_null(resources)

        c = 0
        for line in self.interpolate_command_lines(environment, self.command_lines):
            c = c + 1
            ret = self.call_single(line, environment, resources, callback)
            if self.check_required and ret != 0:
                raise RuntimeError('cannot call sub process, return code = ' + hex(ret))

        if c == 0:
            callback.warn('no command lines')

    def call_single(
            self,
            line: str,
            environment: Environment,
            resources: list,
            callback: CallbackLogger
    ) -> int:
        """通过命令行执行程序

        :param line: 待执行的命令行。
        :param environment: 执行命令相关的环境变量集合。
        :param resources: 执行命令所需要的资源列表。
        :param callback: 用于输出日志的回调接口。
        :return: 此命令的执行结果。
        """
        raise NotImplementedError
